package tr.gov.tubitak.uekae.applet;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import tr.gov.tubitak.uekae.esya.api.asn.x509.ECertificate;
import tr.gov.tubitak.uekae.esya.api.common.ESYAException;
import tr.gov.tubitak.uekae.esya.api.common.util.bag.Pair;
import tr.gov.tubitak.uekae.esya.api.smartcard.apdu.APDUSmartCard;
import tr.gov.tubitak.uekae.esya.api.smartcard.pkcs11.*;

import javax.smartcardio.CardTerminal;
import javax.smartcardio.TerminalFactory;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;

/**
 * SmartCardManager handles user smart card operations. Uses APDU access to card as default.
 * Works with Java 6.
 * PS: Original class has been slightly modified for better incorporation with the applet.
 * @author suleyman.uslu
 */
public class SmartCardManager {

    private static Logger logger = LoggerFactory.getLogger(SmartCardManager.class);

    private static Object lockObject = new Object();

    private static SmartCardManager mSCManager;

    private int mSlotCount = 0;

    public List<ECertificate> mSignatureCerts = new ArrayList<ECertificate>();

    public List<BaseSmartCard> smartCards = new ArrayList<BaseSmartCard>();
    public int cardIndex;

    public HashMap<Integer, String> certTerminalMap = new HashMap<Integer, String>();

    public String[] mTerminals;
    public List<String> mTerminalList = new ArrayList<String>();

    private static boolean useAPDU = true;

    /**
     * Singleton is used for this class. If many card placed, it wants to user to select one of cards.
     * If there is a influential change in the smart card environment, it  repeat the selection process.
     * The influential change can be:
     * 		If there is a new smart card connected to system.
     * 		The cached card is removed from system.
     * These situations are checked in getInstance() function. So for your non-sequential SmartCard Operation,
     * call getInstance() function to check any change in the system.
     *
     * In order to reset these selections, call reset function.
     *
     * @return SmartCardManager instance
     * @throws tr.gov.tubitak.uekae.esya.api.smartcard.pkcs11.SmartCardException
     */
    public static SmartCardManager getInstance() throws SmartCardException
    {
        synchronized (lockObject)
        {
            if(mSCManager == null)
            {
                mSCManager = new SmartCardManager();
                return mSCManager;
            }
            else
            {
                //Check is there any change
                try
                {
                    //If there is a new card in the system, user will select a smartcard.
                    //Create new SmartCard.
                    if(mSCManager.mSlotCount < SmartOp.getCardTerminals().length)
                    {
                        logger.debug("New card pluged in to system");
                        mSCManager = null;
                        return getInstance();
                    }

                    return mSCManager;
                }
                catch (SmartCardException e)
                {
                    mSCManager = null;
                    throw e;
                }
            }
        }
    }


    /**
     * Creates new SmartCardManager object and sets terminal list
     *
     * @throws SmartCardException
     */
    private SmartCardManager() throws SmartCardException
    {
        try
        {
            logger.debug("New SmartCardManager will be created");
            String [] terminals = SmartOp.getCardTerminals();

            if(terminals == null || terminals.length == 0)
                throw new SmartCardException("No terminal with smartcard");

            logger.debug("Number of terminals: " + terminals.length);

            mTerminals = terminals;
            mTerminalList = Arrays.asList(mTerminals);
        }
        catch (SmartCardException e)
        {
            logger.error(e.getMessage());
            throw e;
        }
    }

    public void listCertificatesOfTerminals() throws SmartCardException{

        AccessController.doPrivileged(new PrivilegedAction<Object>() {

            public Object run() {

                cardIndex = -1;
                int certNo = -1;

                for (String terminal : mTerminals) {

                    cardIndex++;
                    try {
                        boolean APDUSupport;

                        try {

                            APDUSupport = APDUSmartCard.isSupported(terminal);

                        } catch (NoClassDefFoundError ex) {
                            logger.error("APDU smartcard cannot be created. Probably AkisCIF.jar is missing");
                            APDUSupport = false;
                        }

                        logger.debug("Smartcard index:" + cardIndex);

                        if (useAPDU == true && APDUSupport) {
                            logger.debug("APDU Smartcard will be created");
                            APDUSmartCard asc = new APDUSmartCard();
                            CardTerminal ct = TerminalFactory.getDefault().terminals().getTerminal(terminal);
                            asc.openSession(ct);
                            smartCards.add(asc);
                        } else {
                            logger.debug("PKCS11 Smartcard will be created");
                            Pair<Long, CardType> slotAndCardType = SmartOp.getSlotAndCardType(terminal);
                            P11SmartCard p11SmartCard = new P11SmartCard(slotAndCardType.getObject2());
                            smartCards.add(p11SmartCard);
                            smartCards.get(cardIndex).openSession(slotAndCardType.getObject1());
                        }

                        // take the certs of a terminal
                        List<ECertificate> certsOfATerminal = getCertificates(cardIndex);

                        // for each cert, save to hashmap which cert belongs to which terminal
                        for (ECertificate certificate : certsOfATerminal) {
                            certNo++;
                            mSCManager.certTerminalMap.put(certNo, terminal);
                        }

                        mSignatureCerts.addAll(certsOfATerminal);
                        // todo burdaki session kapatma islemini kaldirdik, sebebi bir daha session acmak icin beklemeyelim
                        // todo yapilmasi gereken: bi yerlerde diger kart okuyuculara ait sessionlari kapatmak
                        //smartCards.get(cardIndex).closeSession();
                    }
                    catch (Exception e) {
                        logger.error("Error in listing certificates of terminals", e);
                    }
                }
                return null;
            }

        });

    }


    /**
     * Gathers certificates of smartcard with specified index
     * @param index    order of smartcard
     * @return found certificates
     * @throws Exception
     */
    public synchronized List<ECertificate> getCertificates(int index) throws Exception
    {
        List<byte []> allCerts = smartCards.get(index).getSignatureCertificates();

        if(allCerts != null  && allCerts.size() == 0 )
            throw new ESYAException("No certificate in smartcard");

        List<ECertificate> certs = new ArrayList<ECertificate>();

        for (byte[] bs : allCerts)
        {
            ECertificate cert = new ECertificate(bs);
            certs.add(cert);
        }
        return certs;
    }
}
